Skip to content

Conversation

@MatejNedic
Copy link
Member

📢 Type of change

  • Bugfix
  • New feature
  • Enhancement
  • Refactoring

📜 Description

💡 Motivation and Context

💚 How did you test it?

📝 Checklist

  • I reviewed submitted code
  • I added tests to verify changes
  • I updated reference documentation to reflect the change
  • All tests passing
  • No breaking changes

🔮 Next steps

@github-actions github-actions bot added component: s3 S3 integration related issue component: secrets-manager Secrets Manager integration related issue component: sns SNS integration related issue component: sqs SQS integration related issue component: core Core functionality related issue labels Dec 11, 2025
@MatejNedic MatejNedic marked this pull request as ready for review December 17, 2025 15:05
@maciejwalkowiak
Copy link
Contributor

In general great job @MatejNedic! Left two comments plus all new public classes need proper Javadocs

Copy link
Contributor

@tomazfernandes tomazfernandes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @MatejNedic , overall this looks good on the SQS side. I left a couple of questions, mostly around simplifying version detection.

I also think we could prefix the previous classes "Jackson2" rather than Legacy so it's more precise, but that's just a nit.

Let me know what you think.


import org.springframework.messaging.converter.MessageConverter;

public abstract class AbstractMessageConverterFactory {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m curious: could we use direct instantiation based on classpath detection instead of introducing this factory abstraction?

For instance, Spring Kafka’s MessagingMessageConverter handles Jackson version selection via classpath detection:

https://github.com/spring-projects/spring-kafka/blob/fcca99f6ae3d4f370817cfd1e24f2ae5db93efbf/spring-kafka/src/main/java/org/springframework/kafka/support/converter/MessagingMessageConverter.java#L93-L105

Since we already have JacksonPresent too, could we apply the same pattern here so version selection is automatic and users don’t have to manually choose? If there’s a downside or constraint in our case, what is it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking that people can extend AbstractMessageConverterFactory and do their own implementation of MessageConverter. Also, it is much more readable when I refactored to this way considering deletions and so on.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @MatejNedic, I understand what you're aiming at, and we already support users providing their own MessageConverters through the SqsListenerConfigurer.

As far as I can tell, the only place we're using the create() method from AbstractMessageConverterFactory is in AbstractListenerAnnotationBeanPostProcessor.java:345.

I think we could just detect the Jackson version on the classpath there and decide what converter to use automatically.

This might eliminate the need for AbstractMessageConverterFactory, JacksonJsonMessageConverterFactory, and LegacyJackson2MessageConverterFactory, and make overall converter configuration simpler, while still preserving the customization options for users.

What do you think?

Copy link
Member Author

@MatejNedic MatejNedic Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @tomazfernandes , I was thinking about it but I came up with following scenario.

What if you have Jackson 3 and Jackson 2 on classpath. Since this is messaging I want to keep Jackson 2 for SQS integration. currently if we put JacksonPresent users are not able to change this behaviour.

Notice inside of SqsListenerAnnotationBeanPostProcessor we are also based on instace getting ObjectMapper or JsonMapper. I want to allow users to control this. I don't also want to polute EndpointRegistar with ObjectMapper and JsonMapper (we had objectMapper there). So for me best way was to do a wrapper and then extract either Jackson 2 or Jackson 3 based on whichever Bean users defined. This allows users themselves to have on Classpath Jackson 3 and 2 and opt in for 2.

if (wrapper instanceof JacksonJsonMessageConverterFactory) {
			argumentResolvers.add(new NotificationMessageArgumentResolver(messageConverter,
					((JacksonJsonMessageConverterFactory) wrapper).getJsonMapperWrapper().getJsonMapper()));
			argumentResolvers.add(new NotificationSubjectArgumentResolver(
					((JacksonJsonMessageConverterFactory) wrapper).getJsonMapperWrapper().getJsonMapper()));
			argumentResolvers.add(new SnsNotificationArgumentResolver(messageConverter,
					((JacksonJsonMessageConverterFactory) wrapper).getJsonMapperWrapper().getJsonMapper()));
		}
		else if (wrapper instanceof LegacyJackson2MessageConverterFactory) {
			argumentResolvers.add(new LegacyJackson2NotificationMessageArgumentResolver(messageConverter,
					((LegacyJackson2MessageConverterFactory) wrapper).getObjectMapper()));
			argumentResolvers.add(new LegacyJackson2NotificationSubjectArgumentResolver(
					((LegacyJackson2MessageConverterFactory) wrapper).getObjectMapper()));
			argumentResolvers.add(new LegacyJackson2SnsNotificationArgumentResolver(messageConverter,
					((LegacyJackson2MessageConverterFactory) wrapper).getObjectMapper()));
		}
	@tomazfernandes Wdyt?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to allow users to control this.

If the goal is to enable users to force Jackson 2 over Jackson 3 when both are present, a more consistent approach would be exposing this as an option on the EndpointRegistrar, so users can explicitly toggle it via SqsListenerConfigurer. For example JacksonVersion {AUTO, JACKSON_2, JACKSON_3} or something similar. We can also expose the same option as an auto-configuration property if we’d like.

This way we rely on the existing configuration path instead of introducing a new one, and we don’t require users to declare a bean just to flip a configuration toggle. It also makes the selection explicit user intent and keeps the wiring simpler.

If it helps unblock the release we can make this PR focused on the default behavior only (no factory, detect Jackson 2 vs 3, if both are present default to Jackson 3), and add the explicit opt-in toggle in a separate PR.

Let me know your thoughts.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @MatejNedic, can you share the exact 3.x scenario you're referring to, ideally a snippet?

Thanks.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can completely rework this for RC1.

@MatejNedic, appreciate you reworking this for RC1.

For the set later behavior with ObjectMapper, we can keep the 3.x wiring, with both Jackson 2's ObjectMapper (when present) alongside Jackson's 3 JsonMapper in the EndpointRegistrar, and build the appropriate converter.

Let me know if you run into any issues.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MatejNedic, after looking more closely at the classpath and classloading implications, I think this path could introduce CNFE/classpath edge cases across certain dependency combinations, including native/AOT setups.

I also think the current implementation has similar risk: some code paths (for example, instanceof checks against Jackson-specific impl classes) may trigger eager loading/linking of those impls, which can fail when the relevant Jackson dependency is not present.

If timeline allows, I’d prefer releasing this as an M2 so we have room to iterate and gather feedback before RC/GA. If RC1 is the priority, I’m ok shipping as-is and then reassessing for an RC2 if needed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tomazfernandes let's release it as RC1. I believe this solution is good as it is even for GA - the mentioned factory class should be deprecated from the start as this is anyway only intermediary solution for backward compatibility with Jackson 2.

Of course, if we have ideas and capacity to implement a better one, we could do it, but seems like time flies faster than we are able to catch up.

Are you able to give more specific places in code that can introduce CNFE? (I don't see it but i might have missed it).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@maciejwalkowiak agreed on RC1.

While Matej's refactor addressed the instanceof CNFE concerns, I've made some review comments that point out a few remaining points.

the mentioned factory class should be deprecated from the start as this is anyway only intermediary solution for backward compatibility with Jackson 2

Agreed. Let's reflect that in the code by marking it as deprecated and renaming it to something more migration-scoped to make the long-term direction clearer.

@github-actions github-actions bot added the type: documentation Documentation or Samples related issue label Dec 22, 2025
Copy link
Contributor

@tomazfernandes tomazfernandes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a few minor nits.

Copy link
Contributor

@tomazfernandes tomazfernandes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work with the refactor, @MatejNedic! I left some follow-ups. Once those are addressed, I’ll do a final pass and we should be good to go.

* @param messageConverterConfigurer a {@link LegacyJackson2SqsMessagingMessageConverter} consumer.
* @return the builder.
*/
SqsTemplateBuilder configureLegacyJackson2Converter(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment about the javadoc above.

* Set the {@link ObjectMapper} instance to be used for converting the {@link Message} instances payloads.
* @param objectMapper the object mapper instance.
*/
public void setObjectMapper(ObjectMapper objectMapper) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this diff we're removing the ability for the user to set their own mapper in the converter through direct configuration.

I suggest that instead we make it so that if the user sets a JsonMapper, we'll use that to create a new JacksonJsonMessageConverter with the default settings, and then follow a similar path to the current one.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

User should pass objectMapper through the factory. Since now Factory objectMapper is used to be set later on MessageConverter.

Thing is I don't want in this class anything Jackson 2 or Jackson 3 specific. That is why I went with Factroy approach to completely remove any Jackson 2 or 3 classes from here.

In JacksonJsonMessageConverter for Jacskon 3 we cannot set JsonMapper after Converter is created that is why I moved it to factory creation as well so JsonMapper user passed is respected.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

User should pass objectMapper through the factory. Since now Factory objectMapper is used to be set later on MessageConverter.

This is not true for user-instantiated converters, users need a way to manually set it.

Thing is I don't want in this class anything Jackson 2 or Jackson 3 specific.

We can move this method (and the one for the JsonMapper) to the respective subclasses.

@MatejNedic
Copy link
Member Author

@tomazfernandes Thanks on review will apply the changes today! Please check the comments I left.

Copy link
Contributor

@tomazfernandes tomazfernandes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Matej. I’ll take a look.

SQS acceptance criteria for this PR:
• CNFE/classpath risks are mitigated
• No new long-term supported user-facing APIs or extension points are introduced as part of the migration
• No loss of existing user functionality

@tomazfernandes
Copy link
Contributor

tomazfernandes commented Jan 8, 2026

Cool, thanks @MatejNedic.

I’ll re-review after the updates are pushed.

Once the remaining requested changes are addressed, I'll approve the PR and we'll be good to merge.

@tomazfernandes
Copy link
Contributor

tomazfernandes commented Jan 9, 2026

No new long-term supported user-facing APIs or extension points are introduced as part of the migration

@MatejNedic just one more small ask for when you get to it: after the rename and javadoc changes, please move JacksonMessageConverterFactoryAndEnricher and its implementations into the legacy package.

That keeps the migration-only intent clear and makes it easy to remove later.

@MatejNedic
Copy link
Member Author

MatejNedic commented Jan 11, 2026

@tomazfernandes all done!

@github-actions github-actions bot added the type: dependency-upgrade Dependency version bump label Jan 11, 2026
Copy link
Contributor

@tomazfernandes tomazfernandes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @MatejNedic, thanks for the updates.

Re-reviewed. Changes look good. Two issues remain to meet the SQS acceptance criteria for RC/GA, both should be straightforward:

CNFE/classpath risks are mitigated

We’re still hardcoding the Jackson 3 converter in AbstractContainerOptions, which can trigger NoClassDefFoundError when Jackson 3 is not present:

private static final MessagingMessageConverter<?> DEFAULT_MESSAGE_CONVERTER = new SqsMessagingMessageConverter();

No new long-term user-facing APIs introduced as part of the migration

As-is, a user-defined MessagingMessageConverter bean selected in auto-configuration is not propagated into the registrar/BPP argument-resolver path, so conversion there can diverge from what the app is configured to use.

This matters in this PR because otherwise the primary way to influence @SqsListener conversion and Jackson 2 vs 3 selection becomes the migration wrapper rather than the supported MessagingMessageConverter interface, which conflicts with the acceptance criteria above.

Concretely, the fix should be straightforward: I suggest we take the user-provided MessagingMessageConverter bean, wrap via JacksonMessageConverterMigration internally as needed, and thread it through into the registrar/BPP path so both template and listener paths use MessagingMessageConverter as the supported API for converter selection and customization.

After the updates, I’ll re-review quickly. Appreciate your effort.

@MatejNedic
Copy link
Member Author

MatejNedic commented Jan 12, 2026

@tomazfernandes I am afraid I don't understand the part about Registrar.

When I was thinking what abstraction to bring I was solving main problem some of our users will have.

Mutliple Jackson versions on classpath. Now this scenario was already brought to me by 1 of our library users.

Imagine you have.

  1. Scenario
    SqsListener -> Jackson 2
    SqsTemplate -> Jacskon 2
    And a third bean SqsTemplate -> Jackson 3

2 Scenario.
Rolling out services slowly firstly drainign queue and then having producer and consumer upgraded.
SqsListener -> Jackson 2
SqsTemplate -> Jacskon 3

This is something I want to be able to achieve. Currently SqsAutoConfiguration will provide registrar and SqsTemplate with same MessagingMessageConverter. They will be completely the same. I want to allow users to diverge and use both Jacksons in same time.

Autoconfiguration is here only to enforce 2 or 3 not more.

For SqsTemplate

@ConditionalOnMissingBean
@Bean
public SqsTemplate sqsTemplate(SqsAsyncClient sqsAsyncClient,
	ObjectProvider<JacksonMessageConverterMigration> messageConverterFactory,
	ObjectProvider<ObservationRegistry> observationRegistryProvider,
	ObjectProvider<SqsTemplateObservation.Convention> observationConventionProvider,
	MessagingMessageConverter<Message> messageConverter) {
SqsTemplateBuilder builder = SqsTemplate.builder().sqsAsyncClient(sqsAsyncClient)
		.messageConverter(messageConverter);
messageConverterFactory.ifAvailable(mcf -> mcf.configureLegacyObjectMapper(messageConverter));
}

For containerFactory

@ConditionalOnMissingBean
@Bean
public SqsMessageListenerContainerFactory<Object> defaultSqsListenerContainerFactory(
        ObjectProvider<SqsAsyncClient> sqsAsyncClient, ObjectProvider<AsyncErrorHandler<Object>> asyncErrorHandler,
        ObjectProvider<ErrorHandler<Object>> errorHandler,
        ObjectProvider<AsyncMessageInterceptor<Object>> asyncInterceptors,
        ObjectProvider<ObservationRegistry> observationRegistry,
        ObjectProvider<SqsListenerObservation.Convention> observationConventionProvider,
        ObjectProvider<MessageInterceptor<Object>> interceptors,
        ObjectProvider<JacksonMessageConverterMigration> messageConverterFactory,
        MessagingMessageConverter<?> messagingMessageConverter) {

    SqsMessageListenerContainerFactory<Object> factory = new SqsMessageListenerContainerFactory<>();
    factory.configure(this::configureProperties);
    sqsAsyncClient.ifAvailable(factory::setSqsAsyncClient);
    asyncErrorHandler.ifAvailable(factory::setErrorHandler);
    errorHandler.ifAvailable(factory::setErrorHandler);
    interceptors.forEach(factory::addMessageInterceptor);
    asyncInterceptors.forEach(factory::addMessageInterceptor);
    messageConverterFactory.ifAvailable(mcf -> mcf.configureLegacyObjectMapper(messagingMessageConverter));
}		

JacksonMessageConverterMigration is only used to create defaults which were created before and setObjectMapper. Nothing more.

Tbh I wouldn't put MessagingMessageConverter inside JacksonMessageConverterMigration. Personally since at some point we are going to delete that class and instead of it just put JsonMapper and autoconfiguration will look the same.

Also I am trying to follow here Spring Boot way of autoconfiguration. Only simple scenarios are covered by it such as I want only Jackson 3 or Jackson 2 for both. If you want something custom create the Bean of what you need and tweak it how you want. I think this should be intention of our Autoconfiguration IMHO.

@tomazfernandes
Copy link
Contributor

Hey @MatejNedic, thanks for the changes.

As the code you pasted shows, a user-provided MessagingMessageConverter bean is picked up and used by both SqsTemplate and the container factory.

The same MessagingMessageConverter is not wired into the registrar/BPP @SqsListener argument-resolver flow, which creates a discrepancy.

The fix should be straightforward: propagate the selected MessagingMessageConverter into the registrar/BPP path as well, using JacksonMessageConverterMigration as an internal adapter.

This keeps auto-configuration converter customization for the BPP flow and user Jackson 2 vs 3 selection at the MessagingMessageConverter level rather than the temporary migration API, which is what AC #2 requires.

On use cases and auto-configuration, agreed it should stay simple and predictable. The more complex scenarios you mentioned should be handled by users wiring separate beans explicitly.

I'll re-review once the update is pushed. Thanks again.

maciejwalkowiak and others added 4 commits January 13, 2026 22:27
# Conflicts:
#	spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/config/EndpointRegistrar.java
#	spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/converter/AbstractMessagingMessageConverter.java
#	spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/integration/SqsMessageConversionIntegrationTests.java
@tomazfernandes
Copy link
Contributor

@MatejNedic quick update on conflicts:

I resolved the current merge conflicts against awspring/main in a helper PR: MatejNedic#2.

Scope is conflict resolution only (merge upstream/main into the branch and resolve), no intended functional changes beyond that. Tests are green on the SQS module.

I do not have permission to push to your fork branch directly (403), so the easiest path is to merge that helper PR into Introduce-jackson-3-support.

Let me know if you hit any issues merging it.

Resolve conflicts with current main
@MatejNedic
Copy link
Member Author

Hey @tomazfernandes are we good to go after I merged your changes?

@tomazfernandes
Copy link
Contributor

tomazfernandes commented Jan 14, 2026

@MatejNedic thanks for merging the conflict resolution PR.

Clarifying my earlier note on MessagingMessageConverter wiring: no changes needed for the registrar/BPP path, current wiring is good as-is for this PR.

Non-RC blocking follow-up (to be addressed before GA): some Jackson migration Javadocs and reference docs can read like a long-term supported extension surface. That is at odds with our acceptance criteria (“no new long-term supported user-facing APIs or extension points introduced as part of the migration”). Tracked in #1550.

@MatejNedic
Copy link
Member Author

MatejNedic commented Jan 14, 2026

Thank @tomazfernandes on all the support!

Lets not prolong the release and merge this and do release today.

Plan would be the following:
Since it is only docs and classes have been marked with deprecate already, I will create an issue to go through Javadocs and documentation for SQS integration. Will state that that is is only migration api which will be supported till end of 2026. This will be then part of 4.0.0-RC2 release. Since we are not gonna change anything except docs this is not breaking change for RC2 release. I will start the work on it right after we release and give you for the review!

I have got multiple emails about RC1 and jackson 3 support and I want to get this out as soon as possible. Also, API won't change only docs will so I think this should be path forward.

You already created the issue my bad. Merging releasing and getting to work regarding docs!

@MatejNedic MatejNedic merged commit cf7c046 into awspring:main Jan 14, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component: core Core functionality related issue component: dynamodb DynamoDB integration related issue component: parameter-store Parameter Store integration related issue component: s3 S3 integration related issue component: secrets-manager Secrets Manager integration related issue component: sns SNS integration related issue component: sqs SQS integration related issue type: dependency-upgrade Dependency version bump type: documentation Documentation or Samples related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants